# online-booking-tool — project briefing for Claude Code

A ServiceTitan-integrated garage door booking widget, built so that
connecting to a real ServiceTitan tenant later is a config change, not a
rewrite. Split into 3 independent pages that all talk to a local mock
ServiceTitan API over real HTTP.

## File structure

- `landing.html` — the customer-facing booking widget
- `dispatch.html` — ServiceTitan-style dispatch board; polls the mock
  server every ~3.5s so bookings made on `landing.html` show up here live
- `traffic.html` — live view of real request/response JSON as it actually
  happens (not a sample/illustration)
- `st-live-adapter.js` — shared by all three: the real ServiceTitan-shaped API
  client (`LiveDataAdapter`) + small formatting/matching helpers
- `servicetitan-mock/` — local Node/Express mock of the real ServiceTitan
  API (auth, Settings, CRM, JPM, Dispatch)
- `deploy.sh` — `git pull` + `npm install` + `pm2 restart`
- `scenario-model.html` — the original single-file version, kept for
  reference only; not part of the live 3-page setup

## Core architectural rule — do not break this

Every read/write in the three HTML pages goes through `ACTIVE_ADAPTER`
(`LiveDataAdapter`, configured in each page's own script) — never touch
mock server internals directly from a page, and never hardcode data that
should come from the adapter. This is what makes "swap the mock URL for
`https://api.servicetitan.io` and add real credentials" the actual, whole
migration path to a live tenant.

The three pages have **no shared in-page state** — no cross-tab
messaging, no shared JS memory. Each independently calls the same real
mock server. If dispatch.html needs to "know" something landing.html
did, the only correct path is: landing.html writes it to the server,
dispatch.html reads it back on its next poll.

## Coding conventions already established

- Plain ES5 in the browser-side JS (`var`, `function() {}`, no arrow
  functions, no `let`/`const`) — match this in `st-live-adapter.js`,
  `landing.html`, `dispatch.html`, `traffic.html`. The mock server
  (`servicetitan-mock/`) is normal modern Node and uses `const`/arrow
  functions — that's fine, it's a different runtime.
- Inline styles throughout the HTML (no external stylesheet framework) —
  keep matching that pattern rather than introducing a new one.
- Every simplification or unverified assumption is called out in a
  comment at the point it's made — keep doing that. Silent guesses are
  worse than flagged ones.

## Known simplifications / things to verify before a real tenant

- **Client secret sits in each page's `LiveDataAdapter` config.** Fine
  for the local mock; must move behind a backend proxy before any real
  ServiceTitan credentials go near this.
- **Arrival-window time format is assumed** (`"HH:MM:SS"` strings) — the
  real OpenAPI spec only showed a generic `"string"` placeholder. Verify
  against an actual ServiceTitan response before trusting it.
- **GPS-based zone matching (`resolveZoneForCoord`) uses `centerLat`/
  `centerLng`, which is NOT a real ServiceTitan zone field** — it's a
  mock-only convenience standing in for real reverse-geocoding (turning
  a coordinate into a zip, then matching via the real `zips` array,
  which is `resolveZoneForZip` and IS real-API-accurate).
- **Technician photos don't exist in the real API at all** (confirmed by
  checking the actual spec) — every tech always shows a "Photo on file"
  text placeholder, by design, not a bug.
- **Business hours (esp. Sunday 10am–2pm) were picked arbitrarily**, not
  from real client data — worth revisiting with actual hours.
- **Seed data** in the mock server is generated fresh, dated to whatever
  day the server actually starts on (no more "time travel" simulation —
  that was deliberately removed).

## Customer-facing behavior that's load-bearing (don't regress)

- The customer only ever sees an open **time window** — never a menu of
  technicians. `bestTechForWindow()` in `st-live-adapter.js` makes the actual
  skill + zone + load-balance assignment decision invisibly.
- The booking panel's height must never visibly resize between steps —
  this was fixed carefully (fixed `height` + `overflow-y:auto` safety
  net, not `min-height`) after several rounds of it silently breaking.
- No native browser popups/confirms anywhere in the booking flow —
  cancel confirmations, "not you?" flows, etc. are all inline steps.

## Deploying

There are two independent deploy paths to this repo's clones. Both do the
same underlying work (`git pull` + `npm install` + `pm2 restart`
`servicetitan-mock`) — they exist for different situations, not as
competing "correct" ways to deploy.

**Manual — `./deploy.sh`**
```bash
./deploy.sh
```
Run this by hand on any clone after a push. Always works, no dependency on
anything else being set up or running. Use this on a real remote host, or
as a fallback if an auto-watcher (below) isn't running for some reason.

**Automatic — `watch-and-deploy.sh` under pm2**
Some clones (e.g. the local test-server clone on Darren's box) run this
script as a pm2 process (`pm2 start watch-and-deploy.sh --interpreter
bash`) instead of relying on someone running `deploy.sh` by hand. It's a
polling loop, not a webhook: every 5s it does `git fetch origin main`,
compares local `HEAD` to `origin/main`, and if they differ, does the same
pull + install + restart as `deploy.sh`. So changes land within ~5s of a
push, with no manual step.

Known limits of the watcher, worth knowing before relying on it:
- It's a pm2 process, so it survives the chat session that started it,
  but **not a server reboot** — this box has no pm2 boot-persistence
  configured for Windows (`pm2 startup` doesn't support Windows), so a
  reboot drops the watcher (and every other pm2 process on the box) until
  something restarts them.
- It only watches `main`. A push to any other branch is invisible to it.
- It's polling-based — if `git fetch` itself fails (network blip, auth
  expiring), it just silently retries next cycle rather than alerting
  anyone.

In both cases, only the mock server (`servicetitan-mock/`) needs a
restart — the four static files (`landing.html`, `dispatch.html`,
`traffic.html`, `st-live-adapter.js`) are read fresh from disk on every request,
so a browser refresh is always enough for those, with either deploy path.
